/**
  @file App.h

  The G3D 8.0 default starter app is configured for OpenGL 3.0 and relatively recent
  GPUs.  To support older GPUs you may want to disable the framebuffer and film
  classes and use G3D::Sky to handle the skybox.
 */
#ifndef App_h
#define App_h

#include <ctime>
#include <G3D/G3DAll.h>
#include <GLG3D/GLG3D.h>
#include <cutil_inline.h>
#include <cutil_gl_inline.h>
#include <vector_types.h>

#include "FlowSolver.h"
#include "VolumeRenderer.h"
#include "VolumeRadianceTransfer.h"

class VolumeBuffer
{
public:
	GLuint pbo;
	TextureRef tex;
	cudaGraphicsResource *pboRes;

public:
	VolumeBuffer()
	{
		static bool setGLDevice = false;
		if(!setGLDevice)
		{
			cudaGLSetGLDevice(cutGetMaxGflopsDeviceId());
			setGLDevice = true;
		}

		pbo = 0;
		tex = NULL;
		pboRes = NULL;
	}

	~VolumeBuffer()
	{
		free();
	}

public:
	void free()
	{
		if(pbo)
		{
			cutilSafeCall(cudaGraphicsUnregisterResource(pboRes));
			pboRes = NULL;
			glDeleteBuffers(1, &pbo);
			pbo = 0;
		}

		tex = NULL;
	}

	void resize(int nx, int ny, int nz)
	{
		free();
		glGenBuffers(1, &pbo);
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, pbo);
		glBufferData(GL_PIXEL_UNPACK_BUFFER_ARB, nx * ny * nz * sizeof(float), 0, GL_STREAM_DRAW);
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
		cutilSafeCall(cudaGraphicsGLRegisterBuffer(&pboRes, pbo, cudaGraphicsMapFlagsWriteDiscard));
		tex = Texture::fromMemory("", NULL, ImageFormat::L32F(), nx, ny, nz, ImageFormat::L32F(), Texture::DIM_3D_NPOT, Texture::Settings::video());
	}

	void update(int nx, int ny, int nz, float *field)
	{
		if(tex.isNull() || nx != tex->width() || ny != tex->height() || nz != tex->depth())
			resize(nx, ny, nz);

		float *ptr;
		size_t bytes;
		cutilSafeCall(cudaGraphicsMapResources(1, &pboRes, 0));
		cutilSafeCall(cudaGraphicsResourceGetMappedPointer((void **)&ptr, &bytes, pboRes));
		cutilSafeCall(cudaMemcpy(ptr, field, bytes, cudaMemcpyDeviceToDevice));
		cutilSafeCall(cudaGraphicsUnmapResources(1, &pboRes, 0));

		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
		glBindTexture(GL_TEXTURE_3D, tex->openGLID());
		glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, nx, ny, nz, GL_LUMINANCE, GL_FLOAT, 0);
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
	}
};

class App : public GApp
{
protected:
	int fieldIdx, l, m;
	float time, timeStep, sourceRadius, sourceWidth, sourceHeight, sourceDepth;
	float ki, theta, phi;
	bool simulating, indirect;
	Vector3 sourceCenter;
	Field<float> field;
	Array<Field<float>> lmaps;
	FireSolver<float> solver;
	VolumeBuffer volume;
	TFWindow editor;
	VRWindow viewer;
	TextureRef tex;
	GuiDropDownList *fieldList;
	VolumeRadianceTransfer brt;
	Texture::Ref envmap;
	Texture::Ref lhtmap;

public:
    App(const GApp::Settings& settings = GApp::Settings());

    virtual void onInit();
    virtual void onAI();
    virtual void onNetwork();
    virtual void onSimulation(RealTime rdt, SimTime sdt, SimTime idt);
    virtual void onPose(Array<Surface::Ref>& posed3D, Array<Surface2D::Ref>& posed2D);

    // You can override onGraphics if you want more control over the rendering loop.
    // virtual void onGraphics(RenderDevice* rd, Array<Surface::Ref>& surface, Array<Surface2D::Ref>& surface2D);

    virtual void onGraphics3D(RenderDevice* rd, Array<Surface::Ref>& surface);
    virtual void onGraphics2D(RenderDevice* rd, Array<Surface2D::Ref>& surface2D);

    virtual bool onEvent(const GEvent& e);
    virtual void onUserInput(UserInput* ui);
    virtual void onCleanup();

public:
	void updateVolumeTexture()
	{
		int idx = l * l + l + m;
		switch(fieldIdx)
		{
		case 0:
			volume.update(solver.nx, solver.ny, solver.nz, solver.f->ptr);
			break;
		case 1:
			volume.update(solver.nx, solver.ny, solver.nz, solver.s->ptr);
			break;
		case 2:
			volume.update(solver.nx, solver.ny, solver.nz, solver.t->ptr);
			break;
		case 3:
			volume.update(solver.nx, solver.ny, solver.nz, solver.u->ptr);
			break;
		case 4:
			volume.update(solver.nx, solver.ny, solver.nz, solver.v->ptr);
			break;
		case 5:
			volume.update(solver.nx, solver.ny, solver.nz, solver.w->ptr);
			break;
		case 6:
			if(field.ptr)
				volume.update(field.nx, field.ny, field.nz, field.ptr);
			break;
		case 7:
			if(idx >= 0 && idx < 4 && lmaps[idx].ptr)
				volume.update(lmaps[idx].nx, lmaps[idx].ny, lmaps[idx].nz, lmaps[idx].ptr);
			break;
		default:
			return;
		}

		tex = volume.tex;
	}

	void saveField(Field<float> &field, const std::string &path)
	{
		field.save(path);
	}

	void saveField()
	{
		switch(fieldIdx)
		{
		case 0:
			solver.f->save("fuel.dat");
			break;
		case 1:
			solver.s->save("soot.dat");
			break;
		case 2:
			solver.t->save("temp.dat");
			break;
		case 3:
			solver.u->save("velu.dat");
			break;
		case 4:
			solver.v->save("velv.dat");
			break;
		case 5:
			solver.w->save("velw.dat");
			break;
		}
	}
};

#endif
